﻿/*
 Author
 Copyright © David Anson, 2007-2010
 http://blogs.msdn.com/delay/archive/2007/09/10/bringing-a-bit-of-html-to-silverlight-htmltextblock-makes-rich-text-display-easy.aspx
 
 Soauthor
 Copyright © Alexander G. Bykin, Russia 2010
 
 Integrated parts of code DynamicTextBlock
 Copyright © 2009 Pixel Lab http://pixellab.cc
*/

namespace System.Windows.Controls
{
    using System.IO;
    using System.Text;
    using System.Windows;
    using System.Windows.Browser;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Xml;

    public class HtmlBlock : Control
    {
        #region Constants

        private const string elementA = "A";
        private const string elementB = "B";
        private const string elementBR = "BR";
        private const string elementEM = "EM";
        private const string elementI = "I";
        private const string elementP = "P";
        private const string elementSTRONG = "STRONG";
        private const string elementU = "U";
        private const string HTMLTEXTBLOCK_elementTextBlockHtmlTextBlock = "tbHtmlTextBlock";
        private const string HTMLTEXTBLOCK_elementInput = "input";
        private const string HTMLTEXTBLOCK_elementOutput = "output";

        #endregion

        #region Dependency Properties

        #region DependencyProperty LineHeightProperty

        public static DependencyProperty LineHeightProperty =
            DependencyProperty.Register(
            "LineHeight",
            typeof(double),
            typeof(HtmlBlock),
            new PropertyMetadata(OnLineHeightPropertyChanged));

        private static void OnLineHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HtmlBlock htmlBlock = d as HtmlBlock;
            if (htmlBlock == null)
            {
                return;
            }

            if (htmlBlock.tbHtmlTextBlock != null)
            {
                htmlBlock.tbHtmlTextBlock.LineHeight = (double)(e.NewValue);
            }
        }

        #endregion

        #region DependencyProperty LineStackingStrategyProperty

        public static DependencyProperty LineStackingStrategyProperty =
            DependencyProperty.Register(
            "LineStackingStrategy",
            typeof(LineStackingStrategy),
            typeof(HtmlBlock),
            new PropertyMetadata(OnLineStackingStrategyPropertyChanged));

        private static void OnLineStackingStrategyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HtmlBlock htmlBlock = d as HtmlBlock;
            if (htmlBlock == null)
            {
                return;
            }

            if (htmlBlock.tbHtmlTextBlock != null)
            {
                htmlBlock.tbHtmlTextBlock.LineStackingStrategy = (LineStackingStrategy)(e.NewValue);
            }
        }

        #endregion

        #region DependencyProperty TextProperty

        public static DependencyProperty TextProperty =
            DependencyProperty.Register(
            "Text",
            typeof(string),
            typeof(HtmlBlock),
            new PropertyMetadata(OnTextPropertyChanged));

        private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HtmlBlock htmlBlock = d as HtmlBlock;
            if (htmlBlock == null)
            {
                return;
            }

            htmlBlock.ParseAndSetText((string)(e.NewValue));
        }

        #endregion

        #region DependencyProperty TextAlignmentProperty

        public static DependencyProperty TextAlignmentProperty =
            DependencyProperty.Register(
            "TextAlignment",
            typeof(TextAlignment),
            typeof(HtmlBlock),
            new PropertyMetadata(OnTextAlignmentPropertyChanged));

        private static void OnTextAlignmentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HtmlBlock htmlBlock = d as HtmlBlock;
            if (htmlBlock == null)
            {
                return;
            }

            if (htmlBlock.tbHtmlTextBlock != null)
            {
                htmlBlock.tbHtmlTextBlock.TextAlignment = (TextAlignment)(e.NewValue);
            }
        }

        #endregion

        #region DependencyProperty TextDecorationsProperty

        public static DependencyProperty TextDecorationsProperty =
            DependencyProperty.Register(
            "TextDecorations",
            typeof(TextDecorationCollection),
            typeof(HtmlBlock),
            new PropertyMetadata(OnTextDecorationsPropertyChanged));

        private static void OnTextDecorationsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HtmlBlock htmlBlock = d as HtmlBlock;
            if (htmlBlock == null)
            {
                return;
            }

            if (htmlBlock.tbHtmlTextBlock != null)
            {
                htmlBlock.tbHtmlTextBlock.TextDecorations = (TextDecorationCollection)(e.NewValue);
            }
        }

        #endregion

        #region DependencyProperty TextWrappingProperty

        public static DependencyProperty TextWrappingProperty =
            DependencyProperty.Register(
            "TextWrapping",
            typeof(TextWrapping),
            typeof(HtmlBlock),
            new PropertyMetadata(OnTextWrappingPropertyChanged));

        private static void OnTextWrappingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HtmlBlock htmlBlock = d as HtmlBlock;
            if (htmlBlock == null)
            {
                return;
            }

            if (htmlBlock.tbHtmlTextBlock != null)
            {
                htmlBlock.tbHtmlTextBlock.TextWrapping = (TextWrapping)e.NewValue;
            }
        }

        #endregion

        #endregion

        #region Public Properties

        public TextWrapping TextWrapping
        {
            get { return (TextWrapping)GetValue(TextWrappingProperty); }
            set { SetValue(TextWrappingProperty, value); }
        }

        public TextDecorationCollection TextDecorations
        {
            get { return (TextDecorationCollection)GetValue(TextDecorationsProperty); }
            set { SetValue(TextDecorationsProperty, value); }
        }

        public TextAlignment TextAlignment
        {
            get { return (TextAlignment)GetValue(TextAlignmentProperty); }
            set { SetValue(TextAlignmentProperty, value); }
        }

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public LineStackingStrategy LineStackingStrategy
        {
            get { return (LineStackingStrategy)GetValue(LineStackingStrategyProperty); }
            set { SetValue(LineStackingStrategyProperty, value); }
        }

        public double LineHeight
        {
            get { return (double)GetValue(LineHeightProperty); }
            set { SetValue(LineHeightProperty, value); }
        }

        // Returns the visible text (i.e., without the HTML tags)
        public string VisibleText
        {
            get { return this.tbHtmlTextBlock.Text; }
        }

        // Specifies whether the browser DOM can be used to attempt to parse invalid XHTML
        // Note: Deliberately not a DependencyProperty because setting this has security implications
        public bool UseDomAsParser { get; set; }

        // TextBlock properties duplicated so HtmlTextBlock can be used as a TextBlock

        public InlineCollection Inlines
        {
            get { return tbHtmlTextBlock.Inlines; }
        }

        #endregion

        #region Private Properties

        private TextBlock tbHtmlTextBlock { get; set; }
        private string txtOriginalText { get; set; }

        #endregion

        #region Constructors

        public HtmlBlock()
        {
            DefaultStyleKey = typeof(HtmlBlock);

            // Mirror default TextBlock property values
            TextBlock defaultTextBlock = new TextBlock();
            this.LineHeight = defaultTextBlock.LineHeight;
            this.LineStackingStrategy = defaultTextBlock.LineStackingStrategy;
            this.TextAlignment = defaultTextBlock.TextAlignment;
            this.TextDecorations = defaultTextBlock.TextDecorations;
            this.TextWrapping = defaultTextBlock.TextWrapping;
            defaultTextBlock = null;
            if (string.IsNullOrEmpty(this.Name))
            {
                this.Name = Guid.NewGuid().ToString();
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // Get a reference to the embedded TextBlock
            this.tbHtmlTextBlock = GetTemplateChild(HTMLTEXTBLOCK_elementTextBlockHtmlTextBlock) as TextBlock;
        }

        #endregion

        #region Private Methods

        private string GetJSInputElementName()
        {
            return this.Name + HTMLTEXTBLOCK_elementInput;
        }

        private string GetJSOutputElementName()
        {
            return this.Name + HTMLTEXTBLOCK_elementOutput;
        }

        private void ParseAndSetText(string value)
        {
            // Save the original text string
            txtOriginalText = value;
            // Try for a valid XHTML representation of text
            bool success = false;
            try
            {
                // Try to parse as-is
                ParseAndSetSpecifiedText(value);
                success = true;
            }
            catch (XmlException)
            {
                // Invalid XHTML
            }
            if (!success && UseDomAsParser)
            {
                // Fall back on the browser's parsing engine and some custom code
                // Create some DOM nodes to use
                HtmlDocument document = HtmlPage.Document;

                // An invisible DIV to contain all the custom content
                HtmlElement wrapper = document.CreateElement("div");
                wrapper.SetAttribute("id", this.Name + "wrapper");
                wrapper.SetStyleAttribute("display", "none");

                // A DIV to contain the input to the code
                HtmlElement input = document.CreateElement("div");
                wrapper.SetAttribute("id", this.GetJSInputElementName());
                input.SetProperty("innerHTML", value);

                // A DIV to contain the output to the code
                HtmlElement output = document.CreateElement("div");
                wrapper.SetAttribute("id", this.GetJSOutputElementName());

                // There should be only one BODY element, but this is an easy way to handle 0 or more
                foreach (var bodyObject in document.GetElementsByTagName("body"))
                {
                    HtmlElement body = bodyObject as HtmlElement;
                    if (null != body)
                    {
                        // Add wrapper element to the DOM
                        body.AppendChild(wrapper);
                        try
                        {
                            // Add input/output elements to the DOM
                            wrapper.AppendChild(input);
                            wrapper.AppendChild(output);
                            // Simple code for browsers where .innerHTML returns ~XHTML (ex: Firefox)
                            var transformationSimple =
                                "(function(){" +
                                    string.Format("var input = document.getElementById(\"{0}\");", this.GetJSInputElementName()) +
                                    string.Format("var output = document.getElementById(\"{0}\");", this.GetJSOutputElementName()) +
                                    "output.innerHTML = input.innerHTML;" +
                                "})();";
                            // Complex code for browsers where .innerHTML returns content as-is (ex: Internet Explorer)
                            var transformationComplex =
                                "(function(){" +
                                    "function computeInnerXhtml(node, inner) {" +
                                        "if (node.nodeValue) {" +
                                            "return node.nodeValue;" +
                                        "} else if (node.nodeName && (0 < node.nodeName.length)) {" +
                                            "var xhtml = '';" +
                                            "if (node.firstChild) {" +
                                                "if (inner) {" +
                                                    "xhtml += '<' + node.nodeName + '>';" +
                                                "}" +
                                                "var child = node.firstChild;" +
                                                "while (child) {" +
                                                    "xhtml += computeInnerXhtml(child, true);" +
                                                    "child = child.nextSibling;" +
                                                "}" +
                                                "if (inner) {" +
                                                    "xhtml += '</' + node.nodeName + '>';" +
                                                "}" +
                                            "} else {" +
                                                "return ('/' == node.nodeName.charAt(0)) ? ('') : ('<' + node.nodeName + '/>');" +
                                            "}" +
                                            "return xhtml;" +
                                        "}" +
                                    "}" +
                                    string.Format("var input = document.getElementById(\"{0}\");", this.GetJSInputElementName()) +
                                    string.Format("var output = document.getElementById(\"{0}\");", this.GetJSOutputElementName()) +
                                    "output.innerHTML = computeInnerXhtml(input);" +
                                "})();";
                            // Create a list of code options, ordered simple->complex
                            var transformations = new string[] { transformationSimple, transformationComplex };
                            // Try each code option until one works
                            foreach (var transformation in transformations)
                            {
                                // Create a SCRIPT element to contain the code
                                HtmlElement script = document.CreateElement("script");
                                script.SetAttribute("type", "text/javascript");
                                script.SetProperty("text", transformation);
                                // Add it to the wrapper element (which runs the code)
                                wrapper.AppendChild(script);
                                // Get the results of the transformation
                                string xhtml = (string)output.GetProperty("innerHTML") ?? "";
                                // Perform some final transformations for the BR element which browsers get wrong
                                xhtml = xhtml.Replace("<br>", "<br/>");  // Firefox
                                xhtml = xhtml.Replace("<BR>", "<BR/>");  // Internet Explorer
                                try
                                {
                                    // Try to parse
                                    ParseAndSetSpecifiedText(xhtml);
                                    success = true;
                                    break;
                                }
                                catch (XmlException)
                                {
                                    // Still invalid XML
                                }
                            }
                        }
                        finally
                        {
                            // Be sure to remove the wrapper we added to the DOM
                            body.RemoveChild(wrapper);
                        }
                        // Processed one BODY; that's enough
                        break;
                    }
                }
            }
            if (!success)
            {
                // Invalid, unfixable XHTML; display the supplied text as-is
                tbHtmlTextBlock.Inlines.Clear();
                tbHtmlTextBlock.Text = txtOriginalText;
            }
        }

        private void ParseAndSetSpecifiedText(string text)
        {
            // Clear the collection of Inlines
            tbHtmlTextBlock.Inlines.Clear();
            // Wrap the input in a <div> (so even plain text becomes valid XML)
            using (var stringReader = new StringReader(string.Concat("<div>", text, "</div>")))
            {
                // Read the input
                using (var xmlReader = XmlReader.Create(stringReader))
                {
                    // State variables
                    var bold = 0;
                    var italic = 0;
                    var underline = 0;
                    string link = null;
                    var lastElement = elementP;
                    // Read the entire XML DOM...
                    while (xmlReader.Read())
                    {
                        var nameUpper = xmlReader.Name.ToUpper();
                        switch (xmlReader.NodeType)
                        {
                            case XmlNodeType.Element:
                                // Handle the begin element
                                switch (nameUpper)
                                {
                                    case elementA:
                                        link = "";
                                        // Look for the HREF attribute (can't use .MoveToAttribute because it's case-sensitive)
                                        if (xmlReader.MoveToFirstAttribute())
                                        {
                                            do
                                            {
                                                if ("HREF" == xmlReader.Name.ToUpper())
                                                {
                                                    // Store the link target
                                                    link = xmlReader.Value;
                                                    break;
                                                }
                                            } while (xmlReader.MoveToNextAttribute());
                                        }
                                        break;
                                    case elementB:
                                    case elementSTRONG: bold++; break;
                                    case elementI:
                                    case elementEM: italic++; break;
                                    case elementU: underline++; break;
                                    case elementBR: tbHtmlTextBlock.Inlines.Add(new LineBreak()); break;
                                    case elementP:
                                        // Avoid double-space for <p/><p/>
                                        if (lastElement != elementP)
                                        {
                                            tbHtmlTextBlock.Inlines.Add(new LineBreak());
                                            tbHtmlTextBlock.Inlines.Add(new LineBreak());
                                        }
                                        break;
                                }
                                lastElement = nameUpper;
                                break;
                            case XmlNodeType.EndElement:
                                // Handle the end element
                                switch (nameUpper)
                                {
                                    case elementA: link = null; break;
                                    case elementB:
                                    case elementSTRONG: bold--; break;
                                    case elementI:
                                    case elementEM: italic--; break;
                                    case elementU: underline--; break;
                                    case elementBR: tbHtmlTextBlock.Inlines.Add(new LineBreak()); break;
                                    case elementP:
                                        tbHtmlTextBlock.Inlines.Add(new LineBreak());
                                        tbHtmlTextBlock.Inlines.Add(new LineBreak());
                                        break;
                                }
                                lastElement = nameUpper;
                                break;
                            case XmlNodeType.Text:
                            case XmlNodeType.Whitespace:
                                // Create a Run for the visible text
                                // Collapse contiguous whitespace per HTML behavior
                                StringBuilder builder = new StringBuilder(xmlReader.Value.Length);
                                var last = '\0';
                                foreach (char c in xmlReader.Value.Replace('\n', ' '))
                                {
                                    if ((' ' != last) || (' ' != c))
                                    {
                                        builder.Append(c);
                                    }
                                    last = c;
                                }
                                // Trim leading whitespace if following a <P> or <BR> element per HTML behavior
                                var builderString = builder.ToString();
                                if ((elementP == lastElement) || (elementBR == lastElement))
                                {
                                    builderString = builderString.TrimStart();
                                }
                                // If any text remains to display...
                                if (0 < builderString.Length)
                                {
                                    // Create a Run to display it
                                    var run = new Run { Text = builderString };
                                    // Style the Run appropriately
                                    if (0 < bold) run.FontWeight = FontWeights.Bold;
                                    if (0 < italic) run.FontStyle = FontStyles.Italic;
                                    if (0 < underline) run.TextDecorations = System.Windows.TextDecorations.Underline;
                                    if (null != link)
                                    {
                                        // Links get styled and display their HREF since Run doesn't support MouseLeftButton* events
                                        run.TextDecorations = System.Windows.TextDecorations.Underline;
                                        run.Foreground = new SolidColorBrush { Color = Colors.Blue };
                                        run.Text += string.Concat(" <", link, ">");
                                    }
                                    // Add the Run to the collection
                                    tbHtmlTextBlock.Inlines.Add(run);
                                    lastElement = null;
                                }
                                break;
                        }
                    }
                }
            }
        }

        #endregion

        #region Protected Methods
        
        /*#region Protected Method MeasureOverride @ Part of code DynamicTextBlock Copyright © 2009 Pixel Lab

        /// <summary>
        /// Handles the measure part of the measure and arrange layout process. During this process
        /// we measure the textBlock that we've created as content with increasingly smaller amounts
        /// of text until we find text that fits.
        /// </summary>
        /// <param name="availableSize">the available size</param>
        /// <returns>the base implementation of Measure</returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            // just to make the code easier to read
            bool wrapping = this.TextWrapping == TextWrapping.Wrap;

            Size unboundSize = wrapping ? new Size(availableSize.Width, double.PositiveInfinity) : new Size(double.PositiveInfinity, availableSize.Height);
            string reducedText = this.Text;

            // set the text and measure it to see if it fits without alteration
            this.tbHtmlTextBlock.Text = reducedText;
            Size textSize = base.MeasureOverride(unboundSize);

            while (wrapping ? textSize.Height > availableSize.Height : textSize.Width > availableSize.Width)
            {
                int prevLength = reducedText.Length;
                reducedText = this.ReduceText(reducedText);

                if (reducedText.Length == prevLength)
                {
                    break;
                }

                this.tbHtmlTextBlock.Text = reducedText + "...";
                textSize = base.MeasureOverride(unboundSize);
            }

            return base.MeasureOverride(availableSize);
        }

        #endregion

        #region Protected Method ReduceText @ Part of code DynamicTextBlock Copyright © 2009 Pixel Lab

        /// <summary>
        /// Reduces the length of the text. Derived classes can override this to use different techniques 
        /// for reducing the text length.
        /// </summary>
        /// <param name="text">the original text</param>
        /// <returns>the reduced length text</returns>
        protected virtual string ReduceText(string text)
        {
            return text.Substring(0, text.Length - 1);
        }

        #endregion*/
        
        #endregion
    }
}
